﻿/*
cc creativ commons license:
Copyright-Only Dedication* (based on United States law) or Public Domain Certification

The person or persons who have associated work with this document (the "Dedicator" or "Certifier") hereby either (a) certifies that, to the best of his knowledge, the work of authorship identified is in the public domain of the country from which the work is published, or (b) hereby dedicates whatever copyright the dedicators holds in the work of authorship identified below (the "Work") to the public domain.
A certifier, moreover, dedicates any copyright interest he may have in the associated work, and for these purposes, is described as a "dedicator" below. A certifier has taken reasonable steps to verify the copyright status of this work. Certifier recognizes that his good faith efforts may not shield him from liability if in fact the work certified is not in the public domain.
Dedicator makes this dedication for the benefit of the public at large and to the detriment of the Dedicator's heirs and successors.
Dedicator intends this dedication to be an overt act of relinquishment in perpetuity of all present and future rights under copyright law, whether vested or contingent, in the Work.
Dedicator understands that such relinquishment of all rights includes the relinquishment of all rights to enforce (by lawsuit or otherwise) those copyrights in the Work.
Dedicator recognizes that, once placed in the public domain, the Work may be freely reproduced, distributed, transmitted, used, modified, built upon, or otherwise exploited by anyone for any purpose, commercial or non-commercial, and in any way, including by methods that have not yet been invented or conceived.
*/

using System;
using System.Collections;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;

namespace Mapi
{
    public class Mapi
    {
        #region SESSION

        // ----------------------------------------------------------- SESSION ---------

        private const int MapiLogonUi = 0x00000001;
// ReSharper disable once UnusedMember.Local
        private const int MapiPasswordUi = 0x00020000;
        private const int MapiNewSession = 0x00000002;
// ReSharper disable once UnusedMember.Local
        private const int MapiForceDownload = 0x00001000;
// ReSharper disable once UnusedMember.Local
        private const int MapiExtendedUi = 0x00000020;
        private const int MapiDialog = 0x00000008;
// ReSharper disable once UnusedMember.Local
        private const int MapiDialogModeless = 0x00000004 | MapiDialog;


        private IntPtr _session = IntPtr.Zero;
        private IntPtr _winhandle = IntPtr.Zero;

        public bool Logon(IntPtr hwnd)
        {
            _winhandle = hwnd;
            _error = MAPILogon(hwnd, null, null, 0, 0, ref _session);
            if (_error != 0)
            {
                _error = MAPILogon(hwnd, null, null, MapiLogonUi, 0, ref _session);
            }
            return _error == 0;
        }

        public void Reset()
        {
            _findseed = null;
            _origin = new MapiRecipDesc();
            _recpts.Clear();
            _attachs.Clear();
            _lastMsg = null;
        }

        public void Logoff()
        {
            if (_session == IntPtr.Zero)
            {
                return;
            }
            _error = MAPILogoff(_session, _winhandle, 0, 0);
            _session = IntPtr.Zero;
        }


        [DllImport("MAPI32.DLL", CharSet = CharSet.Ansi)]
        private static extern int MAPILogon(IntPtr hwnd, string prf, string pw,
            int flg, int rsv, ref IntPtr sess);

        [DllImport("MAPI32.DLL")]
        private static extern int MAPILogoff(IntPtr sess, IntPtr hwnd,
            int flg, int rsv);

        #endregion

        #region SENDING

        // ----------------------------------------------------------- SENDING ---------

        private const int MapiOrig = 0;
        private const int MapiTo = 1;
        private const int MapiCc = 2;
// ReSharper disable once UnusedMember.Local
        private const int MapiBcc = 3;
        private readonly ArrayList _attachs = new ArrayList();
        private readonly ArrayList _recpts = new ArrayList();
        private MapiRecipDesc _origin = new MapiRecipDesc();

        public int Send(string sub, string txt)
        {
            _lastMsg = new MapiMessage {subject = sub, noteText = txt, originator = AllocOrigin()};

            // set pointers
            _lastMsg.recips = AllocRecips(out _lastMsg.recipCount);
            _lastMsg.files = AllocAttachs(out _lastMsg.fileCount);

            _error = MAPISendMail(_session, _winhandle, _lastMsg, MapiDialog, 0);
            Dealloc();
            Reset();
            return _error;
        }

        public int SendQuiet(string sub, string txt)
        {
            _lastMsg = new MapiMessage {subject = sub, noteText = txt, originator = AllocOrigin()};

            // set pointers
            _lastMsg.recips = AllocRecips(out _lastMsg.recipCount);
            _lastMsg.files = AllocAttachs(out _lastMsg.fileCount);

            _error = MAPISendMail(_session, _winhandle, _lastMsg, MapiNewSession, 0);
            Dealloc();
            Reset();
            return _error;
        }

        public void AddRecip(string name, string addr, bool cc)
        {
            var dest = new MapiRecipDesc {recipClass = cc ? MapiCc : MapiTo, name = name, address = addr};
            _recpts.Add(dest);
        }

        public void SetSender(string sname, string saddr)
        {
            _origin.name = sname;
            _origin.address = saddr;
        }

        public void Attach(string filepath)
        {
            _attachs.Add(filepath);
        }

        private IntPtr AllocOrigin()
        {
            _origin.recipClass = MapiOrig;
            var rtype = typeof (MapiRecipDesc);
            var rsize = Marshal.SizeOf(rtype);
            var ptro = Marshal.AllocHGlobal(rsize);
            Marshal.StructureToPtr(_origin, ptro, false);
            return ptro;
        }

        private IntPtr AllocRecips(out int recipCount)
        {
            recipCount = 0;
            if (_recpts.Count == 0)
            {
                return IntPtr.Zero;
            }

            var rtype = typeof (MapiRecipDesc);
            var rsize = Marshal.SizeOf(rtype);
            var ptrr = Marshal.AllocHGlobal(_recpts.Count*rsize);

            var runptr = (int) ptrr;
            foreach (var r in _recpts)
            {
                Marshal.StructureToPtr(r as MapiRecipDesc, (IntPtr) runptr, false);
                runptr += rsize;
            }

            recipCount = _recpts.Count;
            return ptrr;
        }

        private IntPtr AllocAttachs(out int fileCount)
        {
            fileCount = 0;
            if (_attachs == null)
            {
                return IntPtr.Zero;
            }
            if ((_attachs.Count <= 0) || (_attachs.Count > 100))
            {
                return IntPtr.Zero;
            }

            var atype = typeof (MapiFileDesc);
            var asize = Marshal.SizeOf(atype);
            var ptra = Marshal.AllocHGlobal(_attachs.Count*asize);

            var mfd = new MapiFileDesc {position = -1};
            var runptr = (int) ptra;
            foreach (var a in _attachs)
            {
                var path = a as string;
                mfd.name = Path.GetFileName(path);
                mfd.path = path;
                Marshal.StructureToPtr(mfd, (IntPtr) runptr, false);
                runptr += asize;
            }

            fileCount = _attachs.Count;
            return ptra;
        }

        private void Dealloc()
        {
            var rtype = typeof (MapiRecipDesc);
            var rsize = Marshal.SizeOf(rtype);

            if (_lastMsg.originator != IntPtr.Zero)
            {
                Marshal.DestroyStructure(_lastMsg.originator, rtype);
                Marshal.FreeHGlobal(_lastMsg.originator);
            }

            if (_lastMsg.recips != IntPtr.Zero)
            {
                var runptr = (int) _lastMsg.recips;
                for (var i = 0; i < _lastMsg.recipCount; i++)
                {
                    Marshal.DestroyStructure((IntPtr) runptr, rtype);
                    runptr += rsize;
                }
                Marshal.FreeHGlobal(_lastMsg.recips);
            }

            if (_lastMsg.files != IntPtr.Zero)
            {
                var ftype = typeof (MapiFileDesc);
                var fsize = Marshal.SizeOf(ftype);

                var runptr = (int) _lastMsg.files;
                for (var i = 0; i < _lastMsg.fileCount; i++)
                {
                    Marshal.DestroyStructure((IntPtr) runptr, ftype);
                    runptr += fsize;
                }
                Marshal.FreeHGlobal(_lastMsg.files);
            }
        }

        [DllImport("MAPI32.DLL")]
        private static extern int MAPISendMail(IntPtr sess, IntPtr hwnd,
            MapiMessage message,
            int flg, int rsv);

        #endregion

        #region FINDING

        // ----------------------------------------------------------- FINDING ---------

// ReSharper disable once UnusedMember.Local
        private const int MapiUnreadOnly = 0x00000020;
// ReSharper disable once UnusedMember.Local
        private const int MapiGuaranteeFiFo = 0x00000100;
        private const int MapiLongMsgId = 0x00004000;

        private readonly StringBuilder _lastMsgId = new StringBuilder(600);
        private string _findseed;

        public bool Next(ref MailEnvelop env)
        {
            _error = MAPIFindNext(_session, _winhandle, null, _findseed,
                MapiLongMsgId, 0, _lastMsgId);
            if (_error != 0)
            {
                return false;
            }
            _findseed = _lastMsgId.ToString();

            var ptrmsg = IntPtr.Zero;
            _error = MAPIReadMail(_session, _winhandle, _findseed,
                MapiEnvOnly | MapiPeek | MapiSuprAttach, 0, ref ptrmsg);
            if ((_error != 0) || (ptrmsg == IntPtr.Zero))
            {
                return false;
            }

            _lastMsg = new MapiMessage();
            Marshal.PtrToStructure(ptrmsg, _lastMsg);
            var orig = new MapiRecipDesc();
            if (_lastMsg.originator != IntPtr.Zero)
            {
                Marshal.PtrToStructure(_lastMsg.originator, orig);
            }

            env.Id = _findseed;
            env.Date = DateTime.ParseExact(_lastMsg.dateReceived, "yyyy/MM/dd HH:mm", DateTimeFormatInfo.InvariantInfo);
            env.Subject = _lastMsg.subject;
            env.From = orig.name;
            env.Unread = (_lastMsg.flags & MapiUnread) != 0;
            env.Atts = _lastMsg.fileCount;

            _error = MAPIFreeBuffer(ptrmsg);
            return _error == 0;
        }


        [DllImport("MAPI32.DLL", CharSet = CharSet.Ansi)]
        private static extern int MAPIFindNext(IntPtr sess, IntPtr hwnd, string typ,
            string seed, int flg, int rsv, StringBuilder id);

        #endregion

        #region READING

        // ----------------------------------------------------------- READING ---------

        private const int MapiPeek = 0x00000080;
        private const int MapiSuprAttach = 0x00000800;
        private const int MapiEnvOnly = 0x00000040;
// ReSharper disable once UnusedMember.Local
        private const int MapiBodyAsFile = 0x00000200;

        private const int MapiUnread = 0x00000001;
// ReSharper disable once UnusedMember.Local
        private const int MapiReceiptReq = 0x00000002;
// ReSharper disable once UnusedMember.Local
        private const int MapiSent = 0x00000004;

        private MapiMessage _lastMsg;

        public string Read(string id, out MailAttach[] aat)
        {
            aat = null;
            var ptrmsg = IntPtr.Zero;
            _error = MAPIReadMail(_session, _winhandle, id,
                MapiPeek | MapiSuprAttach, 0, ref ptrmsg);
            if ((_error != 0) || (ptrmsg == IntPtr.Zero))
            {
                return null;
            }

            _lastMsg = new MapiMessage();
            Marshal.PtrToStructure(ptrmsg, _lastMsg);

            if ((_lastMsg.fileCount > 0) && (_lastMsg.fileCount < 100) && (_lastMsg.files != IntPtr.Zero))
            {
                GetAttachNames(out aat);
            }

            MAPIFreeBuffer(ptrmsg);
            return _lastMsg.noteText;
        }

        public bool Delete(string id)
        {
            _error = MAPIDeleteMail(_session, _winhandle, id, 0, 0);
            return _error == 0;
        }

        public bool SaveAttachm(string id, string name, string savepath)
        {
            var ptrmsg = IntPtr.Zero;
            _error = MAPIReadMail(_session, _winhandle, id,
                MapiPeek, 0, ref ptrmsg);
            if ((_error != 0) || (ptrmsg == IntPtr.Zero))
            {
                return false;
            }

            _lastMsg = new MapiMessage();
            Marshal.PtrToStructure(ptrmsg, _lastMsg);
            var f = false;
            if ((_lastMsg.fileCount > 0) && (_lastMsg.fileCount < 100) && (_lastMsg.files != IntPtr.Zero))
            {
                f = SaveAttachByName(name, savepath);
            }
            MAPIFreeBuffer(ptrmsg);
            return f;
        }


        private void GetAttachNames(out MailAttach[] aat)
        {
            aat = new MailAttach[_lastMsg.fileCount];
            var fdtype = typeof (MapiFileDesc);
            var fdsize = Marshal.SizeOf(fdtype);
            var fdtmp = new MapiFileDesc();
            var runptr = (int) _lastMsg.files;
            for (var i = 0; i < _lastMsg.fileCount; i++)
            {
                Marshal.PtrToStructure((IntPtr) runptr, fdtmp);
                runptr += fdsize;
                aat[i] = new MailAttach();
                if (fdtmp.flags != 0)
                {
                    continue;
                }
                aat[i].Position = fdtmp.position;
                aat[i].Name = fdtmp.name;
                aat[i].Path = fdtmp.path;
            }
        }


        private bool SaveAttachByName(string name, string savepath)
        {
            var f = true;
            var fdtype = typeof (MapiFileDesc);
            var fdsize = Marshal.SizeOf(fdtype);
            var fdtmp = new MapiFileDesc();
            var runptr = (int) _lastMsg.files;
            for (var i = 0; i < _lastMsg.fileCount; i++)
            {
                Marshal.PtrToStructure((IntPtr) runptr, fdtmp);
                runptr += fdsize;
                if (fdtmp.flags != 0)
                {
                    continue;
                }
                if (fdtmp.name == null)
                {
                    continue;
                }

                try
                {
                    if (name == fdtmp.name)
                    {
                        if (File.Exists(savepath))
                        {
                            File.Delete(savepath);
                        }
                        File.Move(fdtmp.path, savepath);
                    }
                }
                catch (Exception)
                {
                    f = false;
                    _error = 13;
                }

                try
                {
                    File.Delete(fdtmp.path);
                }
// ReSharper disable once EmptyGeneralCatchClause
                catch (Exception)
                {}
            }
            return f;
        }


        [DllImport("MAPI32.DLL", CharSet = CharSet.Ansi)]
        private static extern int MAPIReadMail(IntPtr sess, IntPtr hwnd, string id,
            int flg, int rsv, ref IntPtr ptrmsg);

        [DllImport("MAPI32.DLL")]
        private static extern int MAPIFreeBuffer(IntPtr ptr);

        [DllImport("MAPI32.DLL", CharSet = CharSet.Ansi)]
        private static extern int MAPIDeleteMail(IntPtr sess, IntPtr hwnd, string id,
            int flg, int rsv);

        #endregion

        #region ADDRESS

        public bool SingleAddress(string label, out string name, out string addr)
        {
            name = null;
            addr = null;
            var newrec = 0;
            var ptrnew = IntPtr.Zero;
            _error = MAPIAddress(_session, _winhandle, null, 1, label, 0, IntPtr.Zero,
                0, 0, ref newrec, ref ptrnew);
            if ((_error != 0) || (newrec < 1) || (ptrnew == IntPtr.Zero))
            {
                return false;
            }

            var recip = new MapiRecipDesc();
            Marshal.PtrToStructure(ptrnew, recip);
            name = recip.name;
            addr = recip.address;

            MAPIFreeBuffer(ptrnew);
            return true;
        }


        [DllImport("MAPI32.DLL", CharSet = CharSet.Ansi)]
        private static extern int MAPIAddress(IntPtr sess, IntPtr hwnd, string caption,
            int editfld, string labels, int recipcount, IntPtr ptrrecips,
            int flg, int rsv, ref int newrec, ref IntPtr ptrnew);

        #endregion

        #region ERRORS

        // ----------------------------------------------------------- ERRORS ---------

        private readonly string[] _errors =
        {
            "OK [0]", "User abort [1]", "General MAPI failure [2]", "MAPI login failure [3]",
            "Disk full [4]", "Insufficient memory [5]", "Access denied [6]", "-unknown- [7]",
            "Too many sessions [8]", "Too many files were specified [9]", "Too many recipients were specified [10]",
            "A specified attachment was not found [11]",
            "Attachment open failure [12]", "Attachment write failure [13]", "Unknown recipient [14]",
            "Bad recipient type [15]",
            "No messages [16]", "Invalid message [17]", "Text too large [18]", "Invalid session [19]",
            "Type not supported [20]", "A recipient was specified ambiguously [21]", "Message in use [22]",
            "Network failure [23]",
            "Invalid edit fields [24]", "Invalid recipients [25]", "Not supported [26]"
        };

        private int _error;

        public string Error()
        {
            if (_error <= 26)
            {
                return _errors[_error];
            }
            return "?unknown? [" + _error + "]";
        }

        #endregion
    }


    // ********************************************* MAPI STRUCTURES *********************************************

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
    public class MapiMessage
    {
        public int reserved;
        public string subject;
        public string noteText;
        public string messageType;
        public string dateReceived;
        public string conversationID;
        public int flags;
        public IntPtr originator; // MapiRecipDesc* [1]
        public int recipCount;
        public IntPtr recips; // MapiRecipDesc* [n]
        public int fileCount;
        public IntPtr files; // MapiFileDesc*  [n]
    }

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
    public class MapiRecipDesc
    {
        public int reserved;
        public int recipClass;
        public string name;
        public string address;
        public int eIDSize;
        public IntPtr entryID; // void*
    }

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
    public class MapiFileDesc
    {
        public int reserved;
        public int flags;
        public int position;
        public string path;
        public string name;
        public IntPtr type;
    }


    // ********************************************* HELPER STRUCTURES *********************************************

    public class MailEnvelop
    {
        public int Atts;
        public DateTime Date;
        public string From;
        public string Id;
        public string Subject;
        public bool Unread;
    }

    public class MailAttach
    {
        public string Name;
        public string Path;
        public int Position;
    }
}